/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.util.designer;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.DefaultListModel;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.WindowConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.JTextComponent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.PMDVersion;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.RuleSet;
import net.sourceforge.pmd.RuleSetFactory;
import net.sourceforge.pmd.RuleSets;
import net.sourceforge.pmd.SourceCodeProcessor;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.LanguageVersionHandler;
import net.sourceforge.pmd.lang.Parser;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.ast.ParseException;
import net.sourceforge.pmd.lang.ast.xpath.Attribute;
import net.sourceforge.pmd.lang.ast.xpath.AttributeAxisIterator;
import net.sourceforge.pmd.lang.dfa.DFAGraphMethod;
import net.sourceforge.pmd.lang.dfa.DFAGraphRule;
import net.sourceforge.pmd.lang.rule.XPathRule;
import net.sourceforge.pmd.lang.symboltable.NameDeclaration;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
import net.sourceforge.pmd.lang.symboltable.Scope;
import net.sourceforge.pmd.lang.symboltable.ScopedNode;
import net.sourceforge.pmd.lang.xpath.Initializer;

@Deprecated // to be removed with PMD 7.0.0
public class Designer implements ClipboardOwner {

    private boolean exitOnClose = true;
    private final CodeEditorTextPane codeEditorPane = new CodeEditorTextPane();
    private final TreeWidget astTreeWidget = new TreeWidget(new Object[0]);
    private DefaultListModel xpathResults = new DefaultListModel();
    private final JList xpathResultList = new JList(xpathResults);
    private final JTextArea xpathQueryArea = new JTextArea(15, 30);
    private final ButtonGroup xpathVersionButtonGroup = new ButtonGroup();
    private final TreeWidget symbolTableTreeWidget = new TreeWidget(new Object[0]);
    private final JFrame frame = new JFrame("PMD Rule Designer (v " + PMDVersion.VERSION + ')');
    private final DFAPanel dfaPanel = new DFAPanel();
    private final JRadioButtonMenuItem[] languageVersionMenuItems = new JRadioButtonMenuItem[getSupportedLanguageVersions().length];
    private static final String SETTINGS_FILE_NAME = System.getProperty("user.home")
            + System.getProperty("file.separator") + ".pmd_designer.xml";

    public Designer(String[] args) {
        if (args.length > 0) {
            exitOnClose = !args[0].equals("-noexitonclose");
        }

        Initializer.initialize();

        xpathQueryArea.setFont(new Font("Verdana", Font.PLAIN, 16));
        JSplitPane controlSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, createCodeEditorPanel(),
                createXPathQueryPanel());

        JSplitPane astAndSymbolTablePane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, createASTPanel(),
                createSymbolTableResultPanel());

        JSplitPane resultsSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, astAndSymbolTablePane,
                createXPathResultPanel());

        JTabbedPane tabbed = new JTabbedPane();
        tabbed.addTab("Abstract Syntax Tree / XPath / Symbol Table", resultsSplitPane);
        tabbed.addTab("Data Flow Analysis", dfaPanel);
        tabbed.setMnemonicAt(0, KeyEvent.VK_A);
        tabbed.setMnemonicAt(1, KeyEvent.VK_D);

        JSplitPane containerSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, controlSplitPane, tabbed);
        containerSplitPane.setContinuousLayout(true);

        JMenuBar menuBar = createMenuBar();
        frame.setJMenuBar(menuBar);
        frame.getContentPane().add(containerSplitPane);
        frame.setDefaultCloseOperation(exitOnClose ? JFrame.EXIT_ON_CLOSE : WindowConstants.DISPOSE_ON_CLOSE);

        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        int screenHeight = screenSize.height;
        int screenWidth = screenSize.width;

        frame.pack();
        frame.setSize(screenWidth * 3 / 4, screenHeight * 3 / 4);
        frame.setLocation((screenWidth - frame.getWidth()) / 2, (screenHeight - frame.getHeight()) / 2);
        frame.setVisible(true);
        int horozontalMiddleLocation = controlSplitPane.getMaximumDividerLocation() * 3 / 5;
        controlSplitPane.setDividerLocation(horozontalMiddleLocation);
        containerSplitPane.setDividerLocation(containerSplitPane.getMaximumDividerLocation() / 2);
        astAndSymbolTablePane.setDividerLocation(astAndSymbolTablePane.getMaximumDividerLocation() / 3);
        resultsSplitPane.setDividerLocation(horozontalMiddleLocation);

        loadSettings();
    }

    private int getDefaultLanguageVersionSelectionIndex() {
        return Arrays.asList(getSupportedLanguageVersions())
                .indexOf(LanguageRegistry.getLanguage("Java").getDefaultVersion());
    }

    private Node getCompilationUnit() {
        LanguageVersionHandler languageVersionHandler = getLanguageVersionHandler();
        return getCompilationUnit(languageVersionHandler);
    }

    static Node getCompilationUnit(LanguageVersionHandler languageVersionHandler, String code) {
        Parser parser = languageVersionHandler.getParser(languageVersionHandler.getDefaultParserOptions());
        Node node = parser.parse(null, new StringReader(code));
        languageVersionHandler.getSymbolFacade().start(node);
        languageVersionHandler.getTypeResolutionFacade(Designer.class.getClassLoader()).start(node);
        return node;
    }

    private Node getCompilationUnit(LanguageVersionHandler languageVersionHandler) {
        return getCompilationUnit(languageVersionHandler, codeEditorPane.getText());
    }

    private static LanguageVersion[] getSupportedLanguageVersions() {
        List<LanguageVersion> languageVersions = new ArrayList<>();
        for (LanguageVersion languageVersion : LanguageRegistry.findAllVersions()) {
            LanguageVersionHandler languageVersionHandler = languageVersion.getLanguageVersionHandler();
            if (languageVersionHandler != null) {
                Parser parser = languageVersionHandler.getParser(languageVersionHandler.getDefaultParserOptions());
                if (parser != null && parser.canParse()) {
                    languageVersions.add(languageVersion);
                }
            }
        }
        return languageVersions.toArray(new LanguageVersion[languageVersions.size()]);
    }

    private LanguageVersion getLanguageVersion() {
        return getSupportedLanguageVersions()[selectedLanguageVersionIndex()];
    }

    private void setLanguageVersion(LanguageVersion languageVersion) {
        if (languageVersion != null) {
            LanguageVersion[] versions = getSupportedLanguageVersions();
            for (int i = 0; i < versions.length; i++) {
                LanguageVersion version = versions[i];
                if (languageVersion.equals(version)) {
                    languageVersionMenuItems[i].setSelected(true);
                    break;
                }
            }
        }
    }

    private int selectedLanguageVersionIndex() {
        for (int i = 0; i < languageVersionMenuItems.length; i++) {
            if (languageVersionMenuItems[i].isSelected()) {
                return i;
            }
        }
        throw new RuntimeException("Initial default language version not specified");
    }

    private LanguageVersionHandler getLanguageVersionHandler() {
        LanguageVersion languageVersion = getLanguageVersion();
        return languageVersion.getLanguageVersionHandler();
    }

    private class ExceptionNode implements TreeNode {

        private Object item;
        private ExceptionNode[] kids;

        ExceptionNode(Object theItem) {
            item = theItem;

            if (item instanceof ParseException) {
                createKids();
            }
        }

        // each line in the error message becomes a separate tree node
        private void createKids() {

            String message = ((ParseException) item).getMessage();
            String[] lines = StringUtils.split(message, PMD.EOL);

            kids = new ExceptionNode[lines.length];
            for (int i = 0; i < lines.length; i++) {
                kids[i] = new ExceptionNode(lines[i]);
            }
        }

        @Override
        public int getChildCount() {
            return kids == null ? 0 : kids.length;
        }

        @Override
        public boolean getAllowsChildren() {
            return false;
        }

        @Override
        public boolean isLeaf() {
            return kids == null;
        }

        @Override
        public TreeNode getParent() {
            return null;
        }

        @Override
        public TreeNode getChildAt(int childIndex) {
            return kids[childIndex];
        }

        public String label() {
            return item.toString();
        }

        @Override
        public Enumeration<TreeNode> children() {
            Enumeration<TreeNode> e = new Enumeration<TreeNode>() {
                int i = 0;

                @Override
                public boolean hasMoreElements() {
                    return kids != null && i < kids.length;
                }

                @Override
                public ExceptionNode nextElement() {
                    return kids[i++];
                }
            };
            return e;
        }

        @Override
        public int getIndex(TreeNode node) {
            for (int i = 0; i < kids.length; i++) {
                if (kids[i] == node) {
                    return i;
                }
            }
            return -1;
        }
    }

    // Tree node that wraps the AST node for the tree widget and
    // any possible children they may have.
    private class ASTTreeNode implements TreeNode {

        private Node node;
        private ASTTreeNode parent;
        private ASTTreeNode[] kids;

        ASTTreeNode(Node theNode) {
            node = theNode;

            Node parent = node.jjtGetParent();
            if (parent != null) {
                this.parent = new ASTTreeNode(parent);
            }
        }

        private ASTTreeNode(ASTTreeNode parent, Node theNode) {
            node = theNode;
            this.parent = parent;
        }

        @Override
        public int getChildCount() {
            return node.jjtGetNumChildren();
        }

        @Override
        public boolean getAllowsChildren() {
            return false;
        }

        @Override
        public boolean isLeaf() {
            return node.jjtGetNumChildren() == 0;
        }

        @Override
        public TreeNode getParent() {
            return parent;
        }

        public Scope getScope() {
            if (node instanceof ScopedNode) {
                return ((ScopedNode) node).getScope();
            }
            return null;
        }

        @Override
        public Enumeration<TreeNode> children() {

            if (getChildCount() > 0) {
                getChildAt(0); // force it to build kids
            }

            Enumeration<TreeNode> e = new Enumeration<TreeNode>() {
                int i = 0;

                @Override
                public boolean hasMoreElements() {
                    return kids != null && i < kids.length;
                }

                @Override
                public ASTTreeNode nextElement() {
                    return kids[i++];
                }
            };
            return e;
        }

        @Override
        public TreeNode getChildAt(int childIndex) {

            if (kids == null) {
                kids = new ASTTreeNode[node.jjtGetNumChildren()];
                for (int i = 0; i < kids.length; i++) {
                    kids[i] = new ASTTreeNode(this.parent, node.jjtGetChild(i));
                }
            }
            return kids[childIndex];
        }

        @Override
        public int getIndex(TreeNode node) {

            for (int i = 0; i < kids.length; i++) {
                if (kids[i] == node) {
                    return i;
                }
            }
            return -1;
        }

        public String label() {
            LanguageVersionHandler languageVersionHandler = getLanguageVersionHandler();
            StringWriter writer = new StringWriter();
            languageVersionHandler.getDumpFacade(writer, "", false).start(node);
            return writer.toString();
        }

        public String getToolTipText() {
            String tooltip = "Line: " + node.getBeginLine() + " Column: " + node.getBeginColumn();
            tooltip += " " + label();
            return tooltip;
        }

        public List<String> getAttributes() {
            List<String> result = new LinkedList<>();
            AttributeAxisIterator attributeAxisIterator = new AttributeAxisIterator(node);
            while (attributeAxisIterator.hasNext()) {
                Attribute attribute = attributeAxisIterator.next();
                result.add(attribute.getName() + "=" + attribute.getStringValue());
            }
            return result;
        }
    }

    private TreeCellRenderer createNoImageTreeCellRenderer() {
        DefaultTreeCellRenderer treeCellRenderer = new DefaultTreeCellRenderer();
        treeCellRenderer.setLeafIcon(null);
        treeCellRenderer.setOpenIcon(null);
        treeCellRenderer.setClosedIcon(null);
        return treeCellRenderer;
    }

    // Special tree variant that knows how to retrieve node labels and
    // provides the ability to expand all nodes at once.
    private class TreeWidget extends JTree {

        private static final long serialVersionUID = 1L;

        TreeWidget(Object[] items) {
            super(items);
            setToolTipText("");
        }

        @Override
        public String convertValueToText(Object value, boolean selected, boolean expanded, boolean leaf, int row,
                boolean hasFocus) {
            if (value == null) {
                return "";
            }
            if (value instanceof ASTTreeNode) {
                return ((ASTTreeNode) value).label();
            }
            if (value instanceof ExceptionNode) {
                return ((ExceptionNode) value).label();
            }
            return value.toString();
        }

        @Override
        public String getToolTipText(MouseEvent e) {
            if (getRowForLocation(e.getX(), e.getY()) == -1) {
                return null;
            }
            TreePath curPath = getPathForLocation(e.getX(), e.getY());
            if (curPath.getLastPathComponent() instanceof ASTTreeNode) {
                return ((ASTTreeNode) curPath.getLastPathComponent()).getToolTipText();
            } else {
                return super.getToolTipText(e);
            }
        }

        public void expandAll(boolean expand) {
            TreeNode root = (TreeNode) getModel().getRoot();
            expandAll(new TreePath(root), expand);
        }

        private void expandAll(TreePath parent, boolean expand) {
            // Traverse children
            TreeNode node = (TreeNode) parent.getLastPathComponent();
            if (node.getChildCount() >= 0) {
                for (Enumeration<? extends TreeNode> e = node.children(); e.hasMoreElements();) {
                    TreeNode n = e.nextElement();
                    TreePath path = parent.pathByAddingChild(n);
                    expandAll(path, expand);
                }
            }

            if (expand) {
                expandPath(parent);
            } else {
                collapsePath(parent);
            }
        }
    }

    private void loadASTTreeData(TreeNode rootNode) {
        astTreeWidget.setModel(new DefaultTreeModel(rootNode));
        astTreeWidget.setRootVisible(true);
        astTreeWidget.expandAll(true);
    }

    private void loadSymbolTableTreeData(TreeNode rootNode) {
        if (rootNode != null) {
            symbolTableTreeWidget.setModel(new DefaultTreeModel(rootNode));
            symbolTableTreeWidget.expandAll(true);
        } else {
            symbolTableTreeWidget.setModel(null);
        }
    }

    private class ShowListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent ae) {
            TreeNode tn;
            try {
                Node lastCompilationUnit = getCompilationUnit();
                tn = new ASTTreeNode(lastCompilationUnit);
            } catch (ParseException pe) {
                tn = new ExceptionNode(pe);
            }

            loadASTTreeData(tn);
            loadSymbolTableTreeData(null);
        }
    }

    private class DFAListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent ae) {

            LanguageVersion languageVersion = getLanguageVersion();
            DFAGraphRule dfaGraphRule = languageVersion.getLanguageVersionHandler().getDFAGraphRule();
            if (dfaGraphRule != null) {
                final RuleSet rs = new RuleSetFactory().createSingleRuleRuleSet(dfaGraphRule);
                RuleContext ctx = new RuleContext();
                ctx.setSourceCodeFilename("[no filename]." + languageVersion.getLanguage().getExtensions().get(0));
                StringReader reader = new StringReader(codeEditorPane.getText());
                PMDConfiguration config = new PMDConfiguration();
                config.setDefaultLanguageVersion(languageVersion);

                try {
                    new SourceCodeProcessor(config).processSourceCode(reader, new RuleSets(rs), ctx);
                    // } catch (PMDException pmde) {
                    // loadTreeData(new ExceptionNode(pmde));
                } catch (Exception e) {
                    e.printStackTrace();
                }

                List<DFAGraphMethod> methods = dfaGraphRule.getMethods();
                if (methods != null && !methods.isEmpty()) {
                    dfaPanel.resetTo(methods, codeEditorPane);
                    dfaPanel.repaint();
                }
            }
        }
    }

    private class XPathListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent ae) {
            xpathResults.clear();
            if (StringUtils.isBlank(xpathQueryArea.getText())) {
                xpathResults.addElement("XPath query field is empty.");
                xpathResultList.repaint();
                codeEditorPane.requestFocus();
                return;
            }
            Node c = getCompilationUnit();
            try {
                XPathRule xpathRule = new XPathRule() {
                    @Override
                    public void addViolation(Object data, Node node, String arg) {
                        xpathResults.addElement(node);
                    }
                };
                xpathRule.setMessage("");
                xpathRule.setLanguage(getLanguageVersion().getLanguage());
                xpathRule.setXPath(xpathQueryArea.getText());
                xpathRule.setVersion(xpathVersionButtonGroup.getSelection().getActionCommand());

                final RuleSet ruleSet = new RuleSetFactory().createSingleRuleRuleSet(xpathRule);

                RuleSets ruleSets = new RuleSets(ruleSet);

                RuleContext ruleContext = new RuleContext();
                ruleContext.setLanguageVersion(getLanguageVersion());

                List<Node> nodes = new ArrayList<>();
                nodes.add(c);
                ruleSets.apply(nodes, ruleContext, xpathRule.getLanguage());

                if (xpathResults.isEmpty()) {
                    xpathResults.addElement("No matching nodes " + System.currentTimeMillis());
                }
            } catch (ParseException pe) {
                xpathResults.addElement(pe.fillInStackTrace().getMessage());
            }
            xpathResultList.repaint();
            xpathQueryArea.requestFocus();
        }
    }

    private class SymbolTableListener implements TreeSelectionListener {
        @Override
        public void valueChanged(TreeSelectionEvent e) {
            if (e.getNewLeadSelectionPath() != null) {
                ASTTreeNode astTreeNode = (ASTTreeNode) e.getNewLeadSelectionPath().getLastPathComponent();

                DefaultMutableTreeNode symbolTableTreeNode = new DefaultMutableTreeNode();
                DefaultMutableTreeNode selectedAstTreeNode = new DefaultMutableTreeNode(
                        "AST Node: " + astTreeNode.label());
                symbolTableTreeNode.add(selectedAstTreeNode);

                List<Scope> scopes = new ArrayList<>();
                Scope scope = astTreeNode.getScope();
                while (scope != null) {
                    scopes.add(scope);
                    scope = scope.getParent();
                }
                Collections.reverse(scopes);
                for (int i = 0; i < scopes.size(); i++) {
                    scope = scopes.get(i);
                    DefaultMutableTreeNode scopeTreeNode = new DefaultMutableTreeNode(
                            "Scope: " + scope.getClass().getSimpleName());
                    selectedAstTreeNode.add(scopeTreeNode);
                    for (Map.Entry<NameDeclaration, List<NameOccurrence>> entry : scope.getDeclarations().entrySet()) {
                        DefaultMutableTreeNode nameDeclarationTreeNode = new DefaultMutableTreeNode(
                                entry.getKey().getClass().getSimpleName() + ": " + entry.getKey());
                        scopeTreeNode.add(nameDeclarationTreeNode);
                        for (NameOccurrence nameOccurrence : entry.getValue()) {
                            DefaultMutableTreeNode nameOccurranceTreeNode = new DefaultMutableTreeNode(
                                    "Name occurrence: " + nameOccurrence);
                            nameDeclarationTreeNode.add(nameOccurranceTreeNode);
                        }
                    }
                }

                List<String> attributes = astTreeNode.getAttributes();
                DefaultMutableTreeNode attributesNode = new DefaultMutableTreeNode(
                        "Attributes (accessible via XPath):");
                selectedAstTreeNode.add(attributesNode);
                for (String attribute : attributes) {
                    attributesNode.add(new DefaultMutableTreeNode(attribute));
                }

                loadSymbolTableTreeData(symbolTableTreeNode);
            }
        }
    }

    private class CodeHighlightListener implements TreeSelectionListener {
        @Override
        public void valueChanged(TreeSelectionEvent e) {
            if (e.getNewLeadSelectionPath() != null) {
                ASTTreeNode selected = (ASTTreeNode) e.getNewLeadSelectionPath().getLastPathComponent();
                if (selected != null) {
                    codeEditorPane.select(selected.node);
                }
            }
        }
    }

    private class ASTListCellRenderer extends JLabel implements ListCellRenderer {
        private static final long serialVersionUID = 1L;

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
                boolean cellHasFocus) {

            if (isSelected) {
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            } else {
                setBackground(list.getBackground());
                setForeground(list.getForeground());
            }

            String text;
            if (value instanceof Node) {
                Node node = (Node) value;
                StringBuffer sb = new StringBuffer();
                String name = node.getClass().getName().substring(node.getClass().getName().lastIndexOf('.') + 1);
                if (Proxy.isProxyClass(value.getClass())) {
                    name = value.toString();
                }
                sb.append(name).append(" at line ").append(node.getBeginLine()).append(" column ")
                        .append(node.getBeginColumn()).append(PMD.EOL);
                text = sb.toString();
            } else {
                text = value.toString();
            }
            setText(text);
            return this;
        }
    }

    private class ASTSelectionListener implements ListSelectionListener {
        @Override
        public void valueChanged(ListSelectionEvent e) {
            ListSelectionModel lsm = (ListSelectionModel) e.getSource();
            if (!lsm.isSelectionEmpty()) {
                Object o = xpathResults.get(lsm.getMinSelectionIndex());
                if (o instanceof Node) {
                    codeEditorPane.select((Node) o);
                }
            }
        }
    }

    private JMenuBar createMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        JMenu menu = new JMenu("Language");
        ButtonGroup group = new ButtonGroup();

        LanguageVersion[] languageVersions = getSupportedLanguageVersions();
        for (int i = 0; i < languageVersions.length; i++) {
            LanguageVersion languageVersion = languageVersions[i];
            JRadioButtonMenuItem button = new JRadioButtonMenuItem(languageVersion.getShortName());
            languageVersionMenuItems[i] = button;
            group.add(button);
            menu.add(button);
        }
        languageVersionMenuItems[getDefaultLanguageVersionSelectionIndex()].setSelected(true);
        menuBar.add(menu);

        JMenu actionsMenu = new JMenu("Actions");
        JMenuItem copyXMLItem = new JMenuItem("Copy xml to clipboard");
        copyXMLItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                copyXmlToClipboard();
            }
        });
        actionsMenu.add(copyXMLItem);
        JMenuItem createRuleXMLItem = new JMenuItem("Create rule XML");
        createRuleXMLItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                createRuleXML();
            }
        });
        actionsMenu.add(createRuleXMLItem);
        menuBar.add(actionsMenu);

        return menuBar;
    }

    private void createRuleXML() {
        CreateXMLRulePanel rulePanel = new CreateXMLRulePanel(xpathQueryArea, codeEditorPane);
        JFrame xmlframe = new JFrame("Create XML Rule");
        xmlframe.setContentPane(rulePanel);
        xmlframe.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        xmlframe.setSize(new Dimension(600, 700));
        xmlframe.addComponentListener(new java.awt.event.ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                JFrame tmp = (JFrame) e.getSource();
                if (tmp.getWidth() < 600 || tmp.getHeight() < 700) {
                    tmp.setSize(600, 700);
                }
            }
        });
        int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height;
        int screenWidth = Toolkit.getDefaultToolkit().getScreenSize().width;
        xmlframe.pack();
        xmlframe.setLocation((screenWidth - xmlframe.getWidth()) / 2, (screenHeight - xmlframe.getHeight()) / 2);
        xmlframe.setVisible(true);
    }

    private JComponent createCodeEditorPanel() {
        JPanel p = new JPanel();
        p.setLayout(new BorderLayout());
        codeEditorPane.setBorder(BorderFactory.createLineBorder(Color.black));
        makeTextComponentUndoable(codeEditorPane);

        p.add(new JLabel("Source code:"), BorderLayout.NORTH);
        p.add(new JScrollPane(codeEditorPane), BorderLayout.CENTER);

        return p;
    }

    private JComponent createASTPanel() {
        astTreeWidget.setCellRenderer(createNoImageTreeCellRenderer());
        TreeSelectionModel model = astTreeWidget.getSelectionModel();
        model.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        model.addTreeSelectionListener(new SymbolTableListener());
        model.addTreeSelectionListener(new CodeHighlightListener());
        return new JScrollPane(astTreeWidget);
    }

    private JComponent createXPathResultPanel() {
        xpathResults.addElement("No XPath results yet, run an XPath Query first.");
        xpathResultList.setBorder(BorderFactory.createLineBorder(Color.black));
        xpathResultList.setFixedCellWidth(300);
        xpathResultList.setCellRenderer(new ASTListCellRenderer());
        xpathResultList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        xpathResultList.getSelectionModel().addListSelectionListener(new ASTSelectionListener());
        JScrollPane scrollPane = new JScrollPane();
        scrollPane.getViewport().setView(xpathResultList);
        return scrollPane;
    }

    private JPanel createXPathQueryPanel() {
        JPanel p = new JPanel();
        p.setLayout(new BorderLayout());
        xpathQueryArea.setBorder(BorderFactory.createLineBorder(Color.black));
        makeTextComponentUndoable(xpathQueryArea);
        JScrollPane scrollPane = new JScrollPane(xpathQueryArea);
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
        final JButton b = createGoButton();

        JPanel topPanel = new JPanel();
        topPanel.setLayout(new BorderLayout());
        topPanel.add(new JLabel("XPath Query (if any):"), BorderLayout.WEST);
        topPanel.add(createXPathVersionPanel(), BorderLayout.EAST);

        p.add(topPanel, BorderLayout.NORTH);
        p.add(scrollPane, BorderLayout.CENTER);
        p.add(b, BorderLayout.SOUTH);

        return p;
    }

    private JComponent createSymbolTableResultPanel() {
        symbolTableTreeWidget.setCellRenderer(createNoImageTreeCellRenderer());
        return new JScrollPane(symbolTableTreeWidget);
    }

    private JPanel createXPathVersionPanel() {
        JPanel p = new JPanel();
        p.add(new JLabel("XPath Version:"));
        for (Entry<String, String> values : XPathRule.VERSION_DESCRIPTOR.mappings().entrySet()) {
            JRadioButton b = new JRadioButton();
            b.setText(values.getKey());
            b.setActionCommand(b.getText());
            if (values.getKey().equals(XPathRule.VERSION_DESCRIPTOR.defaultValue())) {
                b.setSelected(true);
            }
            xpathVersionButtonGroup.add(b);
            p.add(b);
        }
        return p;
    }

    private JButton createGoButton() {
        JButton b = new JButton("Go");
        b.setMnemonic('g');
        b.addActionListener(new ShowListener());
        b.addActionListener(new XPathListener());
        b.addActionListener(new DFAListener());
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                saveSettings();
            }
        });
        return b;
    }

    private static void makeTextComponentUndoable(JTextComponent textConponent) {
        final UndoManager undoManager = new UndoManager();
        textConponent.getDocument().addUndoableEditListener(new UndoableEditListener() {
            @Override
            public void undoableEditHappened(UndoableEditEvent evt) {
                undoManager.addEdit(evt.getEdit());
            }
        });
        ActionMap actionMap = textConponent.getActionMap();
        InputMap inputMap = textConponent.getInputMap();
        actionMap.put("Undo", new AbstractAction("Undo") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                try {
                    if (undoManager.canUndo()) {
                        undoManager.undo();
                    }
                } catch (CannotUndoException e) {
                }
            }
        });
        inputMap.put(KeyStroke.getKeyStroke("control Z"), "Undo");

        actionMap.put("Redo", new AbstractAction("Redo") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                try {
                    if (undoManager.canRedo()) {
                        undoManager.redo();
                    }
                } catch (CannotRedoException e) {
                }
            }
        });
        inputMap.put(KeyStroke.getKeyStroke("control Y"), "Redo");
    }

    public static void main(String[] args) {
        new Designer(args);
    }

    final void setCodeEditPaneText(String text) {
        codeEditorPane.setText(text);
    }

    private String getXmlTreeCode() {
        if (codeEditorPane.getText() != null && codeEditorPane.getText().trim().length() > 0) {
            Node cu = getCompilationUnit();
            return getXmlTreeCode(cu);
        }
        return null;
    }

    static final String getXmlTreeCode(Node cu) {
        String xml = null;
        if (cu != null) {
            try {
                xml = getXmlString(cu);
            } catch (TransformerException e) {
                e.printStackTrace();
                xml = "Error trying to construct XML representation";
            }
        }
        return xml;
    }

    private void copyXmlToClipboard() {
        String xml = getXmlTreeCode();
        if (xml != null) {
            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(xml), this);
        }
    }

    /**
     * Returns an unformatted xml string (without the declaration)
     *
     * @throws TransformerException
     *             if the XML cannot be converted to a string
     */
    private static String getXmlString(Node node) throws TransformerException {
        StringWriter writer = new StringWriter();

        Source source = new DOMSource(node.getAsDocument());
        Result result = new StreamResult(writer);
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer xformer = transformerFactory.newTransformer();
        xformer.setOutputProperty(OutputKeys.INDENT, "yes");
        xformer.transform(source, result);

        return writer.toString();
    }

    @Override
    public void lostOwnership(Clipboard clipboard, Transferable contents) {
    }

    private void loadSettings() {
        InputStream stream = null;
        try {
            File file = new File(SETTINGS_FILE_NAME);
            if (file.exists()) {
                DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
                stream = new FileInputStream(file);
                Document document = builder.parse(stream);
                Element settingsElement = document.getDocumentElement();
                Element codeElement = (Element) settingsElement.getElementsByTagName("code").item(0);
                Element xpathElement = (Element) settingsElement.getElementsByTagName("xpath").item(0);

                String code = getTextContext(codeElement);
                String languageVersion = codeElement.getAttribute("language-version");
                String xpath = getTextContext(xpathElement);
                String xpathVersion = xpathElement.getAttribute("version");

                codeEditorPane.setText(code);
                setLanguageVersion(LanguageRegistry.findLanguageVersionByTerseName(languageVersion));
                xpathQueryArea.setText(xpath);
                for (Enumeration<AbstractButton> e = xpathVersionButtonGroup.getElements(); e.hasMoreElements();) {
                    AbstractButton button = e.nextElement();
                    if (xpathVersion.equals(button.getActionCommand())) {
                        button.setSelected(true);
                        break;
                    }
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(stream);
        }
    }

    private void saveSettings() {
        try {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            Document document = documentBuilder.newDocument();

            Element settingsElement = document.createElement("settings");
            document.appendChild(settingsElement);

            Element codeElement = document.createElement("code");
            settingsElement.appendChild(codeElement);
            codeElement.setAttribute("language-version", getLanguageVersion().getTerseName());
            codeElement.appendChild(document.createCDATASection(codeEditorPane.getText()));

            Element xpathElement = document.createElement("xpath");
            settingsElement.appendChild(xpathElement);
            xpathElement.setAttribute("version", xpathVersionButtonGroup.getSelection().getActionCommand());
            xpathElement.appendChild(document.createCDATASection(xpathQueryArea.getText()));

            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            // This is as close to pretty printing as we'll get using standard
            // Java APIs.
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "3");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

            Source source = new DOMSource(document);
            Result result = new StreamResult(new FileWriter(new File(SETTINGS_FILE_NAME)));
            transformer.transform(source, result);
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TransformerException e) {
            e.printStackTrace();
        }
    }

    private String getTextContext(Element element) {
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < element.getChildNodes().getLength(); i++) {
            org.w3c.dom.Node child = element.getChildNodes().item(i);
            if (child instanceof Text) {
                buf.append(((Text) child).getData());
            }
        }
        return buf.toString();
    }
}
